/*
    Copyright (c) 2011, SpiceSoft Inc.
    All rights reserved.

    Copyright (c) 2009, Orbitz World Wide
    All rights reserved.

    Redistribution and use in source and binary forms, with or without modification, 
    are permitted provided that the following conditions are met:

        * Redistributions of source code must retain the above copyright notice, 
          this list of conditions and the following disclaimer.
        * Redistributions in binary form must reproduce the above copyright notice, 
          this list of conditions and the following disclaimer in the documentation 
          and/or other materials provided with the distribution.
        * Neither the name of the SpiceSoft Inc. nor the names of its contributors 
          may be used to endorse or promote products derived from this software 
          without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


	This code was derived from code:

    http://code.google.com/p/protobuf-java-format/source/browse/trunk/protobuf-java-format/src/java/com/google/protobuf/XmlFormat.java?spec=svn43&r=38

 	Which is licenced under the same licence by Orbitz World Wide
*/

package com.spicesoft.protobuf;

import java.io.IOException;
import java.io.StringWriter;
import java.math.BigInteger;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import com.google.protobuf.UnknownFieldSet;
import com.google.protobuf.Descriptors.EnumValueDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;

/**
 * Encodes a Protocol Buffer into XML (using Dom4j API).
 */
public final class ProtobufToXml {

	/**
	 * Converts a protobuf message to a Dom4j Document
	 * 
	 * @param message
	 *            a protobuf message
	 * @return a Dom4j Document
	 * @throws IOException
	 */
	public static Document print(Message message) throws IOException {
		Document result = DocumentHelper.createDocument();
		final String messageName = message.getDescriptorForType().getName();
		Element messageElement = result.addElement(messageName);
		printContents(message, messageElement);
		return result;
	}

	/**
	 * Converts a protobuf message to XML
	 * 
	 * @param message
	 *            a protobuf message
	 * @param pretty
	 *            whether pretty formatting should be used
	 * @throws IOException
	 * @return XML
	 */
	public static String printToString(Message message, boolean pretty) {
		try {
			OutputFormat format;
			if(pretty) {
				format = OutputFormat.createPrettyPrint();
			} else {
				format = OutputFormat.createCompactFormat();
			}
			format.setEncoding("UTF-8");
			format.setTrimText(false);
			format.setPadText(false);
			StringWriter sw = new StringWriter();
			XMLWriter xmlWriter = new XMLWriter(sw, format);
			xmlWriter.write(print(message));
			xmlWriter.flush();
			return sw.toString();
		} catch(IOException e) {
			throw new RuntimeException("Writing to a StringBuilder threw an IOException (should never happen).", e);
		}
	}

	private static void printContents(Message message, Element messageElement) throws IOException {
		for(Map.Entry<FieldDescriptor, Object> field : message.getAllFields().entrySet()) {
			printField(field.getKey(), field.getValue(), messageElement);
		}
		printUnknownFields(message.getUnknownFields(), messageElement);
	}

	private static void printField(FieldDescriptor field, Object value, Element destination) throws IOException {
		if(field.isRepeated()) {
			Element fieldElement = destination.addElement(field.getName() + "List");
			for(Object element : (List<?>) value) {
				printSingleField(field, element, fieldElement);
			}
		} else {
			printSingleField(field, value, destination);
		}
	}

	private static void printSingleField(FieldDescriptor field, Object value, Element destination) throws IOException {
		Element fieldE;
		if(field.isExtension()) {
			fieldE = destination.addElement("extension");
			// We special-case MessageSet elements for compatibility with
			// proto1.
			if(field.getContainingType().getOptions().getMessageSetWireFormat()
					&& (field.getType() == FieldDescriptor.Type.MESSAGE) && (field.isOptional())
					// object equality
					&& (field.getExtensionScope() == field.getMessageType())) {
				fieldE.addAttribute("type", field.getMessageType().getFullName());
			} else {
				fieldE.addAttribute("type", field.getFullName());
			}
		} else {
			if(field.getType() == FieldDescriptor.Type.GROUP) {
				// Groups must be serialized with their original capitalization.
				fieldE = destination.addElement(field.getMessageType().getName());
			} else {
				fieldE = destination.addElement(field.getName());
			}
		}

		printFieldValue(field, value, fieldE);
	}

	private static void printFieldValue(FieldDescriptor field, Object value, Element fieldE) throws IOException {
		switch(field.getType()) {
		case INT32:
		case INT64:
		case SINT32:
		case SINT64:
		case SFIXED32:
		case SFIXED64:
		case FLOAT:
		case DOUBLE:
		case BOOL:
			// Good old toString() does what we want for these types.
			fieldE.setText(value.toString());
			break;
		case UINT32:
		case FIXED32:
			fieldE.setText(unsignedToString((Integer) value));
			break;
		case UINT64:
		case FIXED64:
			fieldE.setText(unsignedToString((Long) value));
			break;
		case STRING:
			fieldE.setText((String) value);
			break;
		case BYTES: {
			fieldE.setText(((ByteString) value).toStringUtf8());
			break;
		}
		case ENUM: {
			fieldE.setText(((EnumValueDescriptor) value).getName());
			break;
		}
		case MESSAGE:
		case GROUP:
			printContents((Message) value, fieldE);
			break;
		}
	}

	private static void printUnknownFields(UnknownFieldSet unknownFields, Element messageElement) throws IOException {
		for(Map.Entry<Integer, UnknownFieldSet.Field> entry : unknownFields.asMap().entrySet()) {
			UnknownFieldSet.Field field = entry.getValue();

			final String key = entry.getKey().toString();
			for(long value : field.getVarintList()) {
				printUnknownField(key, unsignedToString(value), messageElement);
			}
			for(int value : field.getFixed32List()) {
				printUnknownField(key, String.format((Locale) null, "0x%08x", value), messageElement);
			}
			for(long value : field.getFixed64List()) {
				printUnknownField(key, String.format((Locale) null, "0x%016x", value), messageElement);
			}
			for(ByteString value : field.getLengthDelimitedList()) {
				printUnknownField(key, value.toStringUtf8(), messageElement);
			}
			for(UnknownFieldSet value : field.getGroupList()) {
				Element unknownFieldE = messageElement.addElement("unknown-field");
				unknownFieldE.addAttribute("index", key);
				printUnknownFields(value, unknownFieldE);
			}
		}
	}

	private static void printUnknownField(String fieldKey, String fieldValue, Element destinationE) throws IOException {
		Element unknownFieldE = destinationE.addElement("unknown-field");
		unknownFieldE.addAttribute("index", fieldKey);
		unknownFieldE.setText(fieldValue);
	}

	/**
	 * Convert an unsigned 32-bit integer to a string.
	 */
	private static String unsignedToString(int value) {
		if(value >= 0) {
			return Integer.toString(value);
		} else {
			return Long.toString((value) & 0x00000000FFFFFFFFL);
		}
	}

	/**
	 * Convert an unsigned 64-bit integer to a string.
	 */
	private static String unsignedToString(long value) {
		if(value >= 0) {
			return Long.toString(value);
		} else {
			// Pull off the most-significant bit so that BigInteger doesn't
			// think
			// the number is negative, then set it again using setBit().
			return BigInteger.valueOf(value & 0x7FFFFFFFFFFFFFFFL).setBit(63).toString();
		}
	}

}
